package planning.heuristic;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import planning.util.MachineUsageData;
import planning.util.PlanIOHandler;
import provisioning.DPS;
import provisioning.Monitor;
import provisioning.OptimalProvisioningSystemForHeterogeneousMachines;

import commons.cloud.MachineType;
import commons.cloud.Provider;
import commons.cloud.User;
import commons.config.Configuration;
import commons.io.Checkpointer;
import commons.sim.SimpleSimulator;
import commons.sim.components.LoadBalancer;
import commons.sim.jeevent.JEEventScheduler;
import commons.sim.util.SimulatorProperties;

/**
 * This class represents the heuristic that plans reservation evaluating machine utilization
 * over a planning period (e.g., one year)
 * @author David Candeia
 *
 */
public class RanjanLongTermHeuristic implements PlanningHeuristic{

	private Map<MachineType, Integer> plan;
	private MachineUsageData machineData;
	private final Monitor monitor;

	private Map<MachineType, Map<Long, Double>> hoursUsed;//map used to save CPU consumption information during the planning period
	private double utilizationTarget;//Utilization target
	
	/**
	 * Default constructor. 
	 * @param scheduler Event scheduler
	 * @param monitor Application monitor (DPS)
	 * @param loadBalancers Load balancers used in simulation
	 */
	public RanjanLongTermHeuristic(JEEventScheduler scheduler, Monitor monitor, LoadBalancer[] loadBalancers){
		this.monitor = monitor;
		
		this.plan = new HashMap<MachineType, Integer>();
		
		try {
			machineData = PlanIOHandler.getMachineData();
		} catch (IOException e) {
		} catch (ClassNotFoundException e) {
		}
		if(machineData == null){
			machineData = new MachineUsageData();
		}
		
		this.hoursUsed = new HashMap<MachineType, Map<Long,Double>>();
		this.utilizationTarget = Configuration.getInstance().getDouble(SimulatorProperties.PLANNING_TARGET_UTILIZATION);
//		System.err.println(">> TESTE: target "+utilizationTarget);
	}
	
	@Override
	public void findPlan(Provider[] cloudProviders, User[] cloudUsers) {
		
		//Simulating ...
		DPS dps = (DPS) this.monitor;
		
		SimpleSimulator simulator = (SimpleSimulator) Checkpointer.loadApplication();
		
		dps.registerConfigurable(simulator);
		
		simulator.start();
		
		//Calculating machines usage data
		LoadBalancer[] loadBalancers = simulator.getTiers(); 
		calculateMachinesUsage();
		Configuration config = Configuration.getInstance();
		
		if(Checkpointer.loadSimulationInfo().isFinishDay()){//Simulation finished!
			
			//Creates reservation plan
			calculateMachinesToReserve(config, loadBalancers);
			
			Checkpointer.clear();
			PlanIOHandler.clear();
			try {
				PlanIOHandler.createPlanFile(this.plan, Checkpointer.loadProviders());
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
			
			
		}else{//Persist data to other round
			persisDataToNextRound(loadBalancers, config);
		}
	}
	
	/**
	 * Considers that machines which utilization is higher than the target will be aggregated and then a new amount
	 * of machines to be reserved is calculated!
	 * @param config Simulation configurations
	 * @param loadBalancers 
	 */
	private void calculateMachinesToReserve(Configuration config, LoadBalancer[] loadBalancers) {
		long planningPeriod = config.getLong(SimulatorProperties.PLANNING_PERIOD);
		Map<MachineType, Long> highestNumberOfMachinesUsedPerType = new HashMap<MachineType, Long>();
		
		Map<MachineType, Map<Long, Double>> hoursUsed = this.machineData.getMachineUsagePerType();
		this.hoursUsed = new HashMap<MachineType, Map<Long, Double>>(hoursUsed);
		
		//Updating hours used
		for(MachineType machineType : hoursUsed.keySet()){
			Map<Long, Double> usageData = hoursUsed.get(machineType);
			Map<Long, Double> newUsageData = new HashMap<Long, Double>();
			
			ArrayList<Long> numberOfMachinesList = new ArrayList<Long>(usageData.keySet());
			Collections.sort(numberOfMachinesList);
			Collections.reverse(numberOfMachinesList);
			
			highestNumberOfMachinesUsedPerType.put(machineType, numberOfMachinesList.get(0));
			
			newUsageData.put(numberOfMachinesList.get(0), usageData.get(numberOfMachinesList.get(0)));//First value
			
			for(Long numberOfMachinesUsed : numberOfMachinesList){//Iterating from the highest number of machines to the lowest one
				double higherUsed = usageData.get(numberOfMachinesUsed);
				
				for(long i = numberOfMachinesUsed - 1; i > 0; i--){//If 15 machines were used for 10 hours, 10 machines were also used for 10 hours ...
					Double lowerUsed = (newUsageData.get(i) != null) ? newUsageData.get(i) : usageData.get(i);
					
					if(lowerUsed != null){
						newUsageData.put(i, lowerUsed+higherUsed);
					}
				}
			}
			
			hoursUsed.put(machineType, newUsageData);
		}
		
		//One tier is assumed
		long totalRequestsFinished = loadBalancers[0].getTotalRequestsFinished();
		long totalRequestsArrived = loadBalancers[0].getTotalRequestsArrived();
		
		//Calculating mean machines utilization
		for(MachineType type : hoursUsed.keySet()){
			Map<Long, Double> machinesConsumption = hoursUsed.get(type);
			
			//Capturing the total amount of machines used in the infrastructure
			ArrayList<Long> amountOfMachinesUsed = new ArrayList<Long>(machinesConsumption.keySet());
			Collections.sort(amountOfMachinesUsed);
			long numberOfMachinesUsed = amountOfMachinesUsed.get(amountOfMachinesUsed.size()-1);
			
			//First machines utilization
			double totalUtilization = 0d;
			Long firstAmount = amountOfMachinesUsed.get(0);
			totalUtilization += firstAmount * (machinesConsumption.get(firstAmount) / (type.getTotalCapacity() * planningPeriod * 24)); 
			
			//Other machines utilization (other indexes) must consider that previous machines utilization (previous index) was already computed
			for (int i = 1; i < amountOfMachinesUsed.size(); i++){
				long currentAmount = amountOfMachinesUsed.get(i);
				long previousAmount = amountOfMachinesUsed.get(i-1);
				
				totalUtilization += (currentAmount - previousAmount) * (machinesConsumption.get(currentAmount) / (type.getTotalCapacity() * planningPeriod * 24));
			}
			
//			System.err.println(">> TESTE: totalUtilziation "+totalUtilization);
//			System.err.println(">> TESTE: totalNumberOfServers "+numberOfMachinesUsed);
//			System.err.println(">> TESTE: totalRequestsFinished "+totalRequestsFinished);
//			System.err.println(">> TESTE: totalRequestsArrived "+totalRequestsArrived);
			
			//Deciding amount of machines to reserve according to Ranjan
			double d = (totalUtilization / numberOfMachinesUsed) / totalRequestsFinished;
//			System.err.println(">> TESTE: d "+d);
			double u_l = Math.max(totalRequestsArrived, totalRequestsFinished) * d;
//			System.err.println(">> TESTE: ul "+u_l);
			int n_l = (int) Math.ceil((numberOfMachinesUsed * u_l) / utilizationTarget); 
//			System.err.println(">> TESTE: nl "+n_l + " type " + type);
			
			this.plan.put(type, n_l);
		}
	}
	
	/**
	 * Saving current simulation day information
	 * @param loadBalancers
	 * @param config
	 */
	private void persisDataToNextRound(LoadBalancer[] loadBalancers,
			Configuration config) {
		try {
//			System.err.println(">> TESTE: Persisting arrived "+loadBalancers[0].getTotalRequestsArrived());
//			System.err.println(">> TESTE: Persisting finished "+loadBalancers[0].getTotalRequestsFinished());
			Checkpointer.save();
			Checkpointer.dumpMachineData(this.machineData);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Retrieving amount of machines used by DPS.
	 */
	private void calculateMachinesUsage() {
		//TODO: Change this to collect usage for all machine types
		LinkedList<Integer> usage = ((OptimalProvisioningSystemForHeterogeneousMachines) this.monitor).machinesPerHour;
		for(Integer numberOfMachines : usage){
			machineData.addUsage(MachineType.M1_SMALL, numberOfMachines);
		}
	}

	@Override
	public double getEstimatedProfit(int period) {
		return 0;
	}

	@Override
	public Map<Provider, Map<MachineType, Integer>> getPlan(User[] cloudUsers) {
//		return this.plan;
		return null;
	}
}
